﻿/*

The MIT License

Copyright (c) 2010 Cartesian Analytics, Inc. 

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;

namespace Pipra.Uom.Epsg
{
    /// <summary>
    /// An EPSG unit conversion graph.
    /// </summary>
    public class EpsgUomGraph :
        UomGraph
    {

        private class EpsgUomAuthority : UomFactorAuthorityBase
        {
            private class EpsgUomCollection : KeyedCollection<int,EpsgUom>
            {
                protected override int GetKeyForItem(EpsgUom item)
                {
                    return item.Code;
                }
            }

            private readonly EpsgUomCollection _uoms;

            public EpsgUomAuthority(string uomType, UomComparer comparer)
                : base(uomType, comparer)
            {
                _uoms = new EpsgUomCollection();
            }

            internal void SetEntry(string fromId, string toId, ConversionFactorEntry entry)
            {
                EpsgUom from = GetUnit(fromId);
                EpsgUom to = GetUnit(toId);
                SetEntry(from,to,entry);
            }

            public new EpsgUom GetUnit(string name)
            {
                return base.GetUnit(name) as EpsgUom
                    ?? _uoms.FirstOrDefault(u => Comparer.Equals(u.Name, name))
                ;
            }

            internal void AddUnit(string id, int code)
            {
                AddUnit(new EpsgUom(id,code,this));
            }

            internal void AddUnit(EpsgUom uom)
            {
                if (!_uoms.Contains(uom.Code))
                {
                    _uoms.Add(uom);
                }
            }

            public new IEnumerable<EpsgUom> Units
            {
                get { return _uoms.AsEnumerable();}
            }

        }

        private class EpsgUomAuthorityCollection : KeyedCollection<string, EpsgUomAuthority>
        {

            public EpsgUomAuthorityCollection(UomComparer keyComparer) : base(keyComparer ?? new UomComparer()) { }

            protected override string GetKeyForItem(EpsgUomAuthority item)
            {
                return item.Type;
            }

            public new UomComparer Comparer
            {
                get
                {
                    return base.Comparer as UomComparer;
                }
            }
        }

        private const string UnitCodesResourceName = "Pipra.Uom.Epsg.UnitCodes.xml";
        private const string RelationResourceName = "Pipra.Uom.Epsg.UnitRelations.xml";
        private const string RelationShortcutResourceName = "Pipra.Uom.Epsg.UnitRelationsShortcuts.xml";
        private static readonly Assembly ThisAsm;
        public static readonly EpsgUomGraph Default;

        static EpsgUomGraph()
        {
            ThisAsm = Assembly.GetAssembly(typeof(EpsgUomGraph));
            Default = new EpsgUomGraph();
        }

        private static double ReadFactorString(string value)
        {
            double d;
            if (String.IsNullOrEmpty(value))
            {
                return 1;
            }
            if (String.Equals(value, "pi", StringComparison.OrdinalIgnoreCase))
            {
                return System.Math.PI;
            }
            if (Double.TryParse(value, out d))
            {
                return d;
            }
            throw new FormatException();
        }

        private static void AppendXmlToGraph(Stream xmlStream, EpsgUomAuthorityCollection tableCollection)
        {
            using (var reader = new XmlTextReader(xmlStream))
            {
                reader.MoveToContent();
                while (reader.Read())
                {
                    if (reader.NodeType != XmlNodeType.Element)
                    {
                        continue;
                    }

                    reader.MoveToAttribute("n");
                    string uomTypeName = reader.Value;

                    EpsgUomAuthority table;
                    if (tableCollection.Contains(uomTypeName))
                    {
                        table = tableCollection[uomTypeName];
                    }
                    else
                    {
                        table = new EpsgUomAuthority(uomTypeName, tableCollection.Comparer);
                        tableCollection.Add(table);
                    }

                    reader.MoveToElement();
                    using (XmlReader unitReader = reader.ReadSubtree())
                    {
                        unitReader.Read();
                        while (unitReader.Read())
                        {
                            if (reader.NodeType != XmlNodeType.Element)
                            {
                                continue;
                            }

                            reader.MoveToAttribute("f");
                            string fromUnitId = reader.Value;
                            reader.MoveToAttribute("t");
                            string toUnitId = reader.Value;
                            double factor = ReadFactorString(
                                reader.MoveToAttribute("m")
                                ? reader.Value
                                : null
                            );
                            double divisor = ReadFactorString(
                                reader.MoveToAttribute("d")
                                ? reader.Value
                                : null
                            );
                            table.SetEntry(fromUnitId, toUnitId, new ConversionFactorEntry(factor, divisor));
                        }
                    }
                }
            }
        }

        private static void InitializeGraph(Stream xmlStream, EpsgUomAuthorityCollection tableCollection)
        {
            using (var reader = new XmlTextReader(xmlStream))
            {
                reader.MoveToContent();
                while (reader.Read())
                {
                    if (reader.NodeType != XmlNodeType.Element)
                    {
                        continue;
                    }
                    reader.MoveToAttribute("n");
                    string uomTypeName = reader.Value;
                    reader.MoveToElement();
                    
                    EpsgUomAuthority table;
                    if (tableCollection.Contains(uomTypeName))
                    {
                        table = tableCollection[uomTypeName];
                    }
                    else
                    {
                        table = new EpsgUomAuthority(uomTypeName, tableCollection.Comparer);
                        tableCollection.Add(table);
                    }

                    using (var unitReader = reader.ReadSubtree())
                    {
                        unitReader.Read();
                        while (unitReader.Read())
                        {
                            if (reader.NodeType != XmlNodeType.Element)
                            {
                                continue;
                            }
                            reader.MoveToAttribute("n");
                            string uomId = reader.Value;
                            reader.MoveToAttribute("c");
                            int code = Int32.Parse(reader.Value);
                            table.AddUnit(uomId, code);
                        }
                    }
                }
            }
        }

        private static EpsgUomAuthorityCollection GenerateAuthorityCollection(UomComparer comparer)
        {
            var tableCollection = new EpsgUomAuthorityCollection(comparer);
            InitializeGraph(ThisAsm.GetManifestResourceStream(UnitCodesResourceName), tableCollection);
            AppendXmlToGraph(ThisAsm.GetManifestResourceStream(RelationResourceName), tableCollection);
            AppendXmlToGraph(ThisAsm.GetManifestResourceStream(RelationShortcutResourceName), tableCollection);
            return tableCollection;
        }

        public static IEnumerable<IUomAuthority> BuildAuthorities()
        {
            return BuildAuthorities(new UomComparer());
        }

        public static IEnumerable<IUomAuthority> BuildAuthorities(UomComparer comparer)
        {
            return GenerateAuthorityCollection(comparer).Cast<IUomAuthority>();
        }

        private readonly EpsgUomAuthorityCollection _coreLookup;

        /// <summary>
        /// Constructs the default EPSG unit of measure graph.
        /// </summary>
        public EpsgUomGraph()
            : this((UomComparer)null) { }

        /// <summary>
        /// Constructs an EPSG unit of measure graph with the given UoM <paramref name="comparer"/>.
        /// </summary>
        /// <param name="comparer">The unit of measure comparer.</param>
        public EpsgUomGraph(UomComparer comparer)
            : this(GenerateAuthorityCollection(comparer)) { }

        private EpsgUomGraph(EpsgUomAuthorityCollection coreLookup)
            : base(coreLookup.Cast<IUomAuthority>(),coreLookup.Comparer)
        {
            _coreLookup = coreLookup;
        }

        public EpsgUom GetUnit(int code)
        {
            return _coreLookup.SelectMany(t => t.Units).FirstOrDefault(u => u.Code == code);
        }

        public new EpsgUom GetUnit(string type, string name)
        {
            return _coreLookup.Contains(type) ? _coreLookup[type].GetUnit(name) : null;
        }
    }
}
